在研究了有关测试的相关内容,发现有些被忽略掉的很明显的问题。
1、为什么需要测试
从积极的方面来说,写自动化测试的优势如下:
使重构更简单 —— 你可以自信的修改实现细节,而不用去触及公有 API。
避免代码恶化—— 恶化在什么时候发生?在你修改代码的时候。
提供了可执行的说明和文档 —— * 你在什么时候更想知道软件实际上是如何工作的?在你想修改它们的时候
减少了创建软件的时间 —— 怎么减少时间的?是通过更快速地修改你的代码,出错时测试会自信地告诉你哪里出错了
降低了创建软件的代价 —— 好吧,时间就是金钱,我的朋友
其实上述优势最大的就是,当我们需要优化代码,进行修改时,会给予我们一种保障。所以在自动化测试的支撑下,我们需要做的就比较简单了。
2、测试的最佳实践指导
有 5 条被认为是每个测试都应该遵守的基本原则。便于记忆这 5 条规则的缩写是: F.I.R.S.T.
测试应该:
很快速(Fast) —— 测试应该能够被经常执行。
能隔离(Isolated) —— 测试本身不能依赖于外部因素或者其他测试的结果。
可重复(Repeatable) —— 每次运行测试都应该产生相同的结果。
带自检(Self-verifying) —— 测试应该包括断言,不需要人为干预。
够及时(Timely) —— 测试应该和生产代码一同书写。
坏的实践
不要测试私有方法
私有方法意味着私有。如果你感到有必要测试一个私有方法,那么那个私有方法一定含有概念性错误,通常是作为私有方法,它做的太多了, 从而违背了单一职责原则
如果仅仅为了测试,就让私有方法变为公有,那么就会改变方法的私有性,会有被其他类调用的风险。那么应该该如何去做呢。
通过这个类的公有 API。永远通过公有 API 测试你的代码。程序的公有 API 定义了一个约定,它是一组关于你的程序对应于不同输入时定义良好的一组期望。私有 API (私有方法或者整个类) 并没有定义约定,并且可以不经通知自行修改,所以你的测试 (或者你的同事) 不能依赖于它们。
通过这种方法测试你的私有方法,你可以自由地修改你的 (真正的) 私有代码,并且通过划分成只做一件事情,并经过正确测试的小的类,来提升代码的设计。
不要Stub 私有方法
Stub 私有方法和测试私有方法具有相同的危害,更重要的是,stub 私有方法将会使程序难以调试。
同样,当我们 stub 一个方法的时候,我们必须依据它做出的约定来进行。但是私有方法没有指定的约定的 —— 毕竟,这也是为什么它们是私有的原因。由于私有方法的行为可以不经通知自行修改,你的 stub 可能与实际情况背道而驰,但是你的测试仍然会通过。这是多么的可怕啊,让我们来看一个例子:
今天:一个类的公有方法依赖于该类的一个私有方法。这个私有方法 foo 永远不会返回空。为公有方法编写的测试为了方便起见,我们 stub 出了私有方法。当 stub foo 方法的时候,你永远不会考虑到 foo 返回为空的情况,因为现在这种情况永远不会发生。
明天:这个私有方法被修改了,现在它返回空了。它是一个私有方法,所以这没什么问题。为公有方法编写的测试不会相应地被修改 (“我正在修改一个私有方法,所以我为什么要更新我的测试?”)。公有方法现在在私有方法返回空的情况下会出错,但是测试仍然会通过!
不要 Stub 外部库
第三方代码不应该在你的测试中直接出现。
今天:你的网络部分的代码依赖于著名的 HTTP 库 LSNetworking.为了避免使用实际的网络 (为了让你的测试更快速更可信),你 stub 了那个库中的方法 -[LSNetworking makeGETrequest:],没有通过实际的网络合适地替代了它的行为 (它通过一个封装好的响应调用了执行成功的回调)。
明天:你需要使用一个替代品来取代 LSNetworking (可能是 LSNetworking 已经不再维护或者是你需要换成一个更先进的库,因为它有很多你需要的新特性等等)。这是一次重构,所以你不应该修改测试。你替换了库。你的测试会失败,因为依赖的网络没有被 stub (-[LSNetworking makeGETrequest:]不会被调用)。
应该做什么:测试中,依靠 stubbing 伞 (umbrella stubbing) 来替代那个库的全部功能。
stubbing 伞 (一个我刚刚发明的术语) 包括了对于所有你的代码可能用到的方式 -- 不管事现在还是将来 -- 的 stub。它们可以通过良好声明的 API 完成一些任务,而不去关心实现的细节。
不要测试构造函数
构造函数定义的是实现细节,你不应该测试构造函数,这是因为我们认同测试应该与实现细节解耦这一观点。而且,构造函数不应该包含行为,所以没有值得测试的东西。
今天:你有一个 Car 类,并包含一个构造函数。一旦一个 Car 被创建了,你测试它的 Engine 不为空 (因为你知道构造函数创建了一个新的 Engine 并将它赋给了变量 _engine)。
明天:Engine 类创建起来变得代价很高,所以你决定使用延迟初始化 (lazily initialize),在第一次调用 Engine 的 getter 方法时才初始化 Engine (这是很好的)。 现在为 Car 类的构造函数编写的测试出问题了,即便 Car 类运行良好,但 Car 并没有包括 Engine。另一个可能是你的测试不会失败,因为测试包含 Engine 的 Car 类会触发 Engine 的延迟加载。所以我的问题是:为什么还要测试?
3、如何进行测试
Given / When / Then模式
我们可以根据 Given-When-Then 模式来组织我们的测试用例,将测试用例拆分成三个部分。
在 given 部分里,通过创建模型对象或将被测试的系统设置到指定的状态,来设定测试环境。when 这部分包含了我们要测试的代码。在大部分情况,这里只有一个方法调用。在 then 这部分中 ,我们需要检查我们行为的结果:是否得到了我们期望的结果?对象是否有改变?这部分主要包括一些断言。简单一点来讲就是,设定环境、调用方法、检测结果。
简单的例子看起来是这个样子的:
- (void)testThatItDoesURLEncoding
{
// given
NSString *searchQuery = @"$content$amp;?@";
HTTPRequest *request = [HTTPRequest requestWithURL:@"/search?q=%@", searchQuery];
// when
NSString *encodedURL = request.URL;
// then
XCTAssertEqualObjects(encodedURL, @"/search?q=%24%26%3F%40");
}
这种简单的模式使我们能够更容易地书写和理解这些测试用例,因为它们都遵循了同样的模式。为了更快地浏览,我们甚至会在每个部分的代码上写上 “given”,“when”,“then” 的注释。通过这种方式,这个方法就能很快被理解。
Mock
在iOS测试中的mock测试框架可以采用OCMock
。
我们用 mock 来管理一个对象的所有依赖项。通过这个方式,我们可以测试这个类在隔离情况下的行为。但是这里有个明显的缺点,那就是当我们修改了一个类后,其他依赖于这个类的类的单元测试不能自动失败。但是关于这一点我们可以通过集成测试来补救,因为它可以测试所有的类。
我们不应该‘过度mock’,也就是说,去 mock 除了被测试的对象的其他对象这样的习惯是要尽量避免的。当我们刚开始的时候,我们经常会这样做,我们甚至会 mock 那些简单到可以作为方法参数的对象。现在我们使用了不少真实的对象,而不是 mock 它们。
4、编写测试是一种投资
我们需要花时间编写和维护它们。我们可以证明这种投资有回报的唯一方法就是我们期望节省时间。将实现细节和测试耦合在一起会减少测试带来的回报,使得那些投资变得不合算,甚至在某些情况下变得一文不值。
在编写测试、重构以及修改系统行为的时候,检查你的测试在面对错误的原因时是失败还是通过,然后退一步问问自己,那些测试是否能够最大化你投资的成果
测试代码量会比被测试代码还要多
目前为止,我们的编码库已经纵横 190 个文件和 18,000 行代码,达到了 544 kB。我们测试部分的代码现在差不多有1,200 kB,大概有被测试代码的两倍。虽然我们还没有完全结束这个项目,但是已经接近尾声。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。